package engine;

import dao.implementations.DaoFactory;
import dao.interfaces.EdgeDao;
import dao.interfaces.InstanceDao;
import dao.interfaces.NodeDao;
import dao.interfaces.UnitDao;
import java.awt.Color;
import java.util.ArrayList;

/**
 * This class is responsible for the data integrity and calculations.
 * The GUI has to communicate with an instance of this class.
 * TODO make it failsafe.
 * @author Johnny
 */
public class Engine {

    private NodeDao nodeDao;
    private EdgeDao edgeDao;
    private InstanceDao instDao;
    private UnitDao unitDao;
    private DaoFactory daoFactory;
    private ArrayList<Node> nodeList;
    private ArrayList<Edge> edgeList;
    private ArrayList<Unit> unitList;
    private ArrayList<Path> unitPath;

    public Engine() {
        daoFactory = new DaoFactory();
        nodeDao = daoFactory.getNodeDao();
        edgeDao = daoFactory.getEdgeDao();
        instDao = daoFactory.getInstDao();
        unitDao = daoFactory.getUnitDao();

        nodeList = nodeDao.getNodes();
        edgeList = edgeDao.getEdges();
        unitList = unitDao.getUnits();

        for (Edge edge : edgeList) {
            edge.recalcCdp();
        }

        // Creating the path list for every unit for faster position prediction
        unitPath = new ArrayList<Path>(unitList.size());
        for (Unit unit : unitList) {
            Path p = new Path(unit, edgeList);
            unitPath.add(p);
        }
    }

    /**
     * Returns the list of units.
     * @return the list of units.
     */
    public ArrayList<Unit> getUnits() {
        return unitList;
    }

    /**
     * Returns the list of nodes.
     * @return the list of nodes.
     */
    public ArrayList<Node> getNodes() {
        return nodeList;
    }

    /**
     * Returns the list of edges.
     * @return the list of edges.
     */
    public ArrayList<Edge> getEdges() {
        return edgeList;
    }

    /**
     * Adds a new node.
     * @param x the x coordinate.
     * @param y the y coordinate.
     * @param name the node.
     * @return the newly created node.
     */
    public Node addNode(float x, float y, String name) {
        Node newNode = nodeDao.addNode(x, y, name);
        nodeList.add(newNode);
        return newNode;
    }

    /**
     * Removes a node that's not part of any edge.
     * @param id the id of the node.
     */
    public void removeNode(int id) {
        nodeDao.removeNode(id);
        nodeList.remove(new Node(id));
    }

    /**
     * Repositions a node.
     * I know it's inefficient, but I will optimize the memory usage later on.
     * @param nodeId the id of the node.
     * @param x the x coordinate.
     * @param y the y coordinate.
     */
    public void setNodePos(int nodeId, float x, float y) {
        int index = nodeList.indexOf(new Node(nodeId));
        if (index != -1) {
            Node node = nodeList.get(index);
            node.setPos(x, y);
            nodeDao.updateNode(node);
        }

        for (Edge edge : edgeList) {
            Node node = edge.hasNode(nodeId);
            if (node != null) {
                node.setPos(x, y);
                edge.recalcCdp();
            }
        }

        ArrayList<Node> unitNodes = getUnitNodes(nodeId);
        for (Node node : unitNodes) {
            node.setPos(x, y);
        }
    }

    /**
     * Renames a node.
     * @param nodeId the id of the node.
     * @param name the name.
     */
    public void setNodeName(int nodeId, String name) {
        int index = nodeList.indexOf(new Node(nodeId));
        if (index != -1) {
            Node node = nodeList.get(index);
            node.setName(name);
            nodeDao.updateNode(node);
        }

        for (Edge edge : edgeList) {
            Node node = edge.hasNode(nodeId);
            if (node != null) {
                node.setName(name);
            }
        }

        ArrayList<Node> unitNodes = getUnitNodes(nodeId);
        for (Node node : unitNodes) {
            node.setName(name);
        }
    }

    /**
     * Returns all the instances of a node from the units list.
     * @return
     */
    private ArrayList<Node> getUnitNodes(int nodeId) {
        ArrayList<Node> list = new ArrayList<Node>();
        for (Unit unit : unitList) {
            Node node = unit.hasNode(nodeId);
            if (node != null) {
                list.add(node);
            }
        }
        return list;
    }

    /**
     * Adds a new unit.
     * @param name the unit.
     * @return the newly created unit.
     */
    public Unit addUnit(String name) {
        Unit newUnit = unitDao.addUnit(name);
        unitList.add(newUnit);
        Path p = new Path(newUnit, edgeList);
        unitPath.add(p);
        return newUnit;
    }

    /**
     * Removes a unit.
     * @param id the id of the unit.
     */
    public void removeUnit(int id) {
        unitDao.removeUnit(id);
        int index = unitList.indexOf(new Unit(id));
        if (index != -1) {
            unitList.remove(index);
            unitPath.remove(index);
        }
    }

    /**
     * Sets the name of a unit.
     * @param unitId the id of the unit.
     * @param name the new name.
     */
    public void setUnitName(int unitId, String name) {
        int index = unitList.indexOf(new Unit(unitId));
        if (index != -1) {
            Unit unit = unitList.get(index);
            unit.setName(name);
            unitDao.updateUnit(unit);
        }
    }

    /**
     * Adds a node to the unit's course list.
     * @param unitId the id of the unit.
     * @param nodeId the id of the node.
     */
    public void addUnitCourse(int unitId, int nodeId) {
        int index = unitList.indexOf(new Unit(unitId));
        int index2 = nodeList.indexOf(new Node(nodeId));
        if (index != -1 && index2 != -1) { // If they exist
            Unit unit = unitList.get(index);
            Node node = nodeList.get(index2);
            unit.addCourse(node);
            unitDao.addToCourse(unitId, nodeId);
            // Retrieveing the new set of instances of the unit
            unit.setInstances(instDao.getInstances(unitId));

            unitPath.get(index).revalidate(edgeList);
        }
    }

    /**
     * Removes a node from the unit's course list.
     * @param unitId the id of the unit.
     * @param nodeId the id of the node.
     */
    public void removeUnitCourse(int unitId, int nodeId) {
        int index = unitList.indexOf(new Unit(unitId));
        if (index != -1) {
            Unit unit = unitList.get(index);
            unit.removeCourse(nodeId);
            unitDao.removeFromCourse(unitId, nodeId);

            unitPath.get(index).revalidate(edgeList);
        }
    }

    /**
     * Adds a new instance to a unit.
     * @param unitId the id of the unit.
     */
    public void addInstance(int unitId) {
        int index = unitList.indexOf(new Unit(unitId));
        if (index != -1) {
            Unit unit = unitList.get(index);
            Instance inst = instDao.addInstance(unitId);
            unit.addInstace(inst);
        }
    }

    /**
     * Removes an instance of a unit.
     * @param unitId the id of the unit.
     * @param instanceId the id of the instance.
     */
    public void removeInstance(int unitId, int instanceId) {
        int index = unitList.indexOf(new Unit(unitId));
        if (index != -1) {
            Unit unit = unitList.get(index);
            int index2 = unit.getInstances().indexOf(new Instance(instanceId));
            if (index2 != -1) {
                unit.getInstances().remove(index2);
                instDao.removeInstance(instanceId);
            }
        }
    }

    /**
     * Sets a schedule for an instance of a unit.
     * @param unitId the id of the unit.
     * @param instanceId the id of the instance.
     * @param times the list containing the updated times.
     */
    public void setSchedule(int unitId, int instanceId, ArrayList<Time> times) {
        int index = unitList.indexOf(new Unit(unitId));
        if (index != -1) {
            Unit unit = unitList.get(index);
            int index2 = unit.getInstances().indexOf(new Instance(instanceId));
            if (index2 != -1) {
                Instance inst = unit.getInstances().get(index2);
                if (times.size() == inst.getScheduleNr()) {
                    inst.setSchedules(times);
                    instDao.setSchedule(inst);
                }
            }
        }
    }

    /**
     * Adds an edge.
     * @param startNodeId the id of it's start-node.
     * @param stopNodeId the id of it's stop-node.
     * @param colorRed the ammount of red from the color.
     * @param colorGreen the ammount of green from the color.
     * @param colorBlue the ammount of blue from the color.
     * @return the newly created edge.
     */
    public Edge addEdge(int startNodeId, int stopNodeId, int colorRed,
            int colorGreen, int colorBlue) {
        Edge edge = edgeDao.addEdge(startNodeId, stopNodeId, colorRed, colorGreen, colorBlue);
        edge.recalcCdp();
        edgeList.add(edge);
        return edge;
    }

    /**
     * Updates an edge's attributes.
     * @param edgeId the id of the edge.
     * @param startNodeId the id of it's start-node.
     * @param stopNodeId the id of it's stop-node.
     * @param colorRed the ammount of red from the color.
     * @param colorGreen the ammount of green from the color.
     * @param colorBlue the ammount of blue from the color.
     */
    public void updateEdge(int edgeId, int startNodeId, int stopNodeId, int colorRed,
            int colorGreen, int colorBlue) {
        int index = edgeList.indexOf(new Edge(edgeId));
        if (index != -1) {
            Edge edge = edgeList.get(index);

            int index2 = nodeList.indexOf(new Node(startNodeId));
            int index3 = nodeList.indexOf(new Node(stopNodeId));
            if (index2 != -1 && index3 != -1) {
                edge.setStartNode(nodeList.get(index2));
                edge.setStopNode(nodeList.get(index3));
                edge.setColor(new Color(colorRed, colorGreen, colorBlue));
                edge.recalcCdp();
                edgeDao.updateEdge(edge);
            }
        }
    }

    /**
     * Removes an edge.
     * @param edgeId the id of the edge.
     */
    public void removeEdge(int edgeId) {
        edgeDao.removeEdge(edgeId);
        edgeList.remove(new Edge(edgeId));
    }

    /**
     * Adds a node to the edge's middle nodes list.
     * @param edgeId the id of the edge.
     * @param nodeId the id of the node.
     */
    public void addMiddleNode(int edgeId, int nodeId) {
        int index = edgeList.indexOf(new Edge(edgeId));
        int index2 = nodeList.indexOf(new Node(nodeId));
        if (index != -1 && index2 != -1) { // If they exist
            Edge edge = edgeList.get(index);
            Node node = nodeList.get(index2);
            edge.addMiddleNode(node);
            edgeDao.addMiddleNode(edgeId, nodeId);
            edge.recalcCdp();
        }
    }

    /**
     * Removes a node from the edge's middle nodes list.
     * @param edgeId the id of the edge.
     * @param nodeId the id of the node.
     */
    public void removeMiddleNode(int edgeId, int nodeId) {
        int index = edgeList.indexOf(new Edge(edgeId));
        if (index != -1) {
            Edge edge = edgeList.get(index);
            edge.removeMiddleNode(nodeId);
            edge.recalcCdp();
            edgeDao.removeMiddleNode(edgeId, nodeId);
        }
    }

    /**
     * Returns the positions of a unit at a specified time.
     * @param unitId the id of the unit.
     * @param now the specified time.
     * @return a list of nodes that represent the position, named after the units.
     */
    public ArrayList<Node> getUnitPos(int unitId, Time now) {
        ArrayList<Node> positions = new ArrayList<Node>();
        int index = unitList.indexOf(new Unit(unitId));
        if (index != -1) {
            Unit unit = unitList.get(index);
            Path p = unitPath.get(index);
            for (Instance inst : unit.getInstances()) {
                LookUpData ld = p.getPos(inst, now);
                if (ld != null) {
                    Node pos = ld.getEdge().getAbstractPos(ld.getF());
                    pos.setName(unit.getName());
                    positions.add(pos);
                }
            }
        }

        return positions;
    }
}
